Skip to content

Commit 8fcee02

Browse files
committed
This attempts to make code ES version agnostic between
7 and 8. We import both python libraries `elasticsearch7` and `elasticsearch8` We test the server version and set it in `settings.py` We use the correct library based on the server major version
1 parent bace5bc commit 8fcee02

File tree

18 files changed

+291
-58
lines changed

18 files changed

+291
-58
lines changed

.env-test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ REUSE_DB=0
2222
ENABLE_ADMIN=True
2323
SET_LOCALE_PATH=False
2424
SECURE_SSL_REDIRECT=False
25-
ES_URLS=http://elasticsearch:9200
25+
ES_URLS=http://elasticsearch:9200

docker-compose.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,18 @@ services:
3434
- pgvolume:/var/lib/postgresql/data
3535

3636
elasticsearch:
37-
image: docker.elastic.co/elasticsearch/elasticsearch:8.18.0
37+
image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2
3838
environment:
3939
- discovery.type=single-node
4040
- LOG4J_FORMAT_MSG_NO_LOOKUPS=true
41-
- xpack.security.enabled=false
42-
- xpack.security.http.ssl.enabled=false
4341
ports:
4442
- "9200:9200"
4543
- "9300:9300"
4644
volumes:
4745
- ./kitsune/search/dictionaries/synonyms:/usr/share/elasticsearch/config/synonyms
4846

4947
kibana:
50-
image: docker.elastic.co/kibana/kibana:8.18.0
48+
image: docker.elastic.co/kibana/kibana:7.10.2
5149
ports:
5250
- 5601:5601
5351
environment:

kitsune/community/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
from django.core.cache import cache
77
from django.db.models import Count, F, Q
88
from django.db.models.functions import Now
9-
from elasticsearch.dsl import A
109

10+
if settings.ES_VERSION == 8:
11+
from elasticsearch8.dsl import A
12+
else:
13+
from elasticsearch_dsl import A
1114
from kitsune.products.models import Product
1215
from kitsune.search.documents import AnswerDocument, ProfileDocument
1316
from kitsune.users.models import ContributionAreas, User

kitsune/questions/models.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@
2121
from django.urls import is_valid_path
2222
from django.utils import translation
2323
from django.utils.translation import pgettext
24-
from elasticsearch import ApiError
24+
25+
if settings.ES_VERSION == 7:
26+
from elasticsearch7 import ElasticsearchException
27+
ESExceptionClass = ElasticsearchException
28+
else:
29+
from elasticsearch8 import ApiError
30+
ESExceptionClass = ApiError
2531
from product_details import product_details
2632

2733
from kitsune.flagit.models import FlaggedObject
@@ -595,7 +601,7 @@ def related_documents(self):
595601
for hit in search[:3].execute().hits
596602
]
597603
cache.set(key, documents, settings.CACHE_LONG_TIMEOUT)
598-
except ApiError:
604+
except ESExceptionClass:
599605
log.exception("ES MLT related_documents")
600606
documents = []
601607

@@ -641,7 +647,7 @@ def related_questions(self):
641647
for hit in search[:3].execute().hits
642648
]
643649
cache.set(key, questions, settings.CACHE_LONG_TIMEOUT)
644-
except ApiError:
650+
except ESExceptionClass:
645651
log.exception("ES MLT related_questions")
646652
questions = []
647653

kitsune/search/base.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@
1010
from django.utils import timezone
1111
from django.utils.translation import gettext as _
1212
from elasticsearch.exceptions import NotFoundError, RequestError
13-
from elasticsearch.dsl import Document as DSLDocument
14-
from elasticsearch.dsl import InnerDoc, MetaField
15-
from elasticsearch.dsl import Search as DSLSearch
16-
from elasticsearch.dsl import field
17-
from elasticsearch.dsl.utils import AttrDict
13+
14+
if settings.ES_VERSION == 8:
15+
from elasticsearch8.dsl import Document as DSLDocument
16+
from elasticsearch8.dsl import InnerDoc, MetaField
17+
from elasticsearch8.dsl import Search as DSLSearch
18+
from elasticsearch8.dsl import field
19+
from elasticsearch8.dsl.utils import AttrDict
20+
else:
21+
from elasticsearch_dsl import Document as DSLDocument
22+
from elasticsearch_dsl import InnerDoc, MetaField
23+
from elasticsearch_dsl import Search as DSLSearch
24+
from elasticsearch_dsl import field
25+
from elasticsearch_dsl.utils import AttrDict
1826
from pyparsing import ParseException
1927

2028
from kitsune.search.config import (
@@ -205,7 +213,10 @@ def to_action(self, action=None, is_bulk=False, **kwargs):
205213
# documents will be updated/added directly in the index.
206214
# For ES8 we need to use the string value "true" instead of boolean
207215
if settings.TEST and not is_bulk:
208-
kwargs.update({"refresh": "true"})
216+
if settings.ES_VERSION >= 8:
217+
kwargs.update({"refresh": "true"})
218+
else:
219+
kwargs.update({"refresh": True})
209220

210221
if not action or action == "index":
211222
return payload if is_bulk else self.save(**kwargs)
@@ -320,7 +331,8 @@ class inherits, relevant to the documents the child class is searching over.
320331
"""
321332

322333
total: int = dfield(default=0, init=False)
323-
hits: AttrDict = dfield(default_factory=list, init=False)
334+
# Use a single declaration with a type that works for both ES versions
335+
hits: Union[list[AttrDict], AttrDict] = dfield(default_factory=list, init=False)
324336
results: list[dict] = dfield(default_factory=list, init=False)
325337
last_key: Union[int, slice, None] = dfield(default=None, init=False)
326338

@@ -394,14 +406,18 @@ def run(self, key: Union[int, slice] = slice(0, settings.SEARCH_RESULTS_PER_PAGE
394406
return self.run(key)
395407
raise e
396408

397-
self.hits = cast(AttrDict, result.hits)
409+
if settings.ES_VERSION >= 8:
410+
self.hits = cast(AttrDict, result.hits)
411+
else:
412+
self.hits = result.hits
398413
self.last_key = key
399414

400415
# Handle total hits according to ES8 response format
401416
# In ES8, total is always returned as an object with a 'value' property
402-
self.total = getattr(self.hits.total, "value", 0)
403-
if isinstance(self.hits.total, dict):
404-
self.total = self.hits.total.get("value", 0)
417+
if settings.ES_VERSION >= 8:
418+
self.total = getattr(self.hits.total, "value", 0)
419+
if isinstance(self.hits.total, dict):
420+
self.total = self.hits.total.get("value", 0)
405421

406422
self.results = [self.make_result(hit) for hit in self.hits]
407423

kitsune/search/documents.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from django.db.models import Count, Prefetch, Q
2-
from elasticsearch.dsl import InnerDoc, connections, field
2+
from django.conf import settings
3+
4+
if settings.ES_VERSION == 8:
5+
from elasticsearch8.dsl import InnerDoc, connections, field
6+
else:
7+
from elasticsearch_dsl import InnerDoc, connections, field
38

49
from kitsune.forums.models import Post
510
from kitsune.questions.models import Answer, Question

kitsune/search/es_utils.py

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
from elasticsearch import Elasticsearch
77
from elasticsearch.helpers import bulk as es_bulk
88
from elasticsearch.helpers.errors import BulkIndexError
9-
from elasticsearch.dsl import Document, UpdateByQuery, analyzer, char_filter, token_filter
9+
10+
if settings.ES_VERSION == 8:
11+
from elasticsearch8.dsl import Document, UpdateByQuery, analyzer, char_filter, token_filter
12+
else:
13+
from elasticsearch_dsl import Document, UpdateByQuery, analyzer, char_filter, token_filter
1014

1115
from kitsune.search import config
1216

@@ -46,6 +50,7 @@ def _create_synonym_graph_filter(synonym_file_name):
4650
synonyms_path=f"synonyms/{synonym_file_name}.txt",
4751
expand="true",
4852
lenient="true",
53+
updateable="true",
4954
)
5055

5156

@@ -94,16 +99,23 @@ def es_client(**kwargs):
9499
if es_cloud_id := settings.ES_CLOUD_ID:
95100
kwargs.update({"cloud_id": es_cloud_id, "basic_auth": settings.ES_HTTP_AUTH})
96101
else:
97-
# Elasticsearch 8.x settings
102+
# Basic ES settings that apply to all versions
98103
es_settings = {
99104
"hosts": settings.ES_URLS,
100-
"request_timeout": settings.ES_TIMEOUT,
101-
"retry_on_timeout": settings.ES_RETRY_ON_TIMEOUT,
102-
# SSL settings - these are needed for ES8 which requires SSL by default
103-
"verify_certs": settings.ES_VERIFY_CERTS,
104-
"ssl_show_warn": settings.ES_SSL_SHOW_WARN,
105105
}
106106

107+
# Add settings that are specific to ES 8+
108+
if getattr(settings, "ES_VERSION", 0) >= 8:
109+
es_settings.update(
110+
{
111+
"request_timeout": settings.ES_TIMEOUT,
112+
"retry_on_timeout": settings.ES_RETRY_ON_TIMEOUT,
113+
# SSL settings - these are needed for ES8 which requires SSL by default
114+
"verify_certs": settings.ES_VERIFY_CERTS,
115+
"ssl_show_warn": settings.ES_SSL_SHOW_WARN,
116+
}
117+
)
118+
107119
if settings.ES_HTTP_AUTH:
108120
es_settings.update({"basic_auth": settings.ES_HTTP_AUTH})
109121

@@ -149,7 +161,10 @@ def index_object(doc_type_name, obj_id):
149161
kwargs = {}
150162
# For ES8, use string "true" instead of boolean True for refresh parameter
151163
if settings.TEST:
152-
kwargs["refresh"] = "true"
164+
if settings.ES_VERSION >= 8:
165+
kwargs["refresh"] = "true"
166+
else:
167+
kwargs["refresh"] = True
153168

154169
if doc_type.update_document:
155170
doc_type.prepare(obj).to_action("update", doc_as_upsert=True, **kwargs)
@@ -191,7 +206,11 @@ def index_objects_bulk(
191206
(doc.to_action(action=action, is_bulk=True, **kwargs) for doc in docs),
192207
chunk_size=elastic_chunk_size,
193208
raise_on_error=False, # we'll raise the errors ourselves, so all the chunks get sent
194-
refresh="true" if settings.TEST else False, # use string "true" for ES8 compatibility
209+
refresh=(
210+
"true"
211+
if settings.TEST and settings.ES_VERSION >= 8
212+
else (True if settings.TEST else False)
213+
), # refresh parameter based on test mode and ES version
195214
)
196215
errors = [
197216
error
@@ -212,17 +231,39 @@ def remove_from_field(doc_type_name, field_name, field_value):
212231
doc_type = next(cls for cls in get_doc_types() if cls.__name__ == doc_type_name)
213232

214233
# Create script as a string
215-
script_source = (
216-
f"if (ctx._source.{field_name} != null) {{ "
217-
f"ctx._source.{field_name}.removeAll(Collections.singleton(params.value)); "
218-
f"}}"
219-
)
234+
if getattr(settings, "ES_VERSION", 0) >= 8:
235+
script_source = (
236+
f"if (ctx._source.{field_name} != null) {{ "
237+
f"ctx._source.{field_name}.removeAll(Collections.singleton(params.value)); "
238+
f"}}"
239+
)
240+
else:
241+
script_source = (
242+
f"if (ctx._source.{field_name}.contains(params.value)) {{"
243+
f"ctx._source.{field_name}.remove(ctx._source.{field_name}.indexOf(params.value))"
244+
f"}}"
245+
)
220246

221247
# Set up the update query
222248
update = UpdateByQuery(using=es_client(), index=doc_type._index._name)
223249

224250
# Apply the script with parameters
225-
update = update.script(source=script_source, lang="painless", params={"value": field_value})
251+
if getattr(settings, "ES_VERSION", 0) >= 8:
252+
update = update.script(
253+
source=script_source, lang="painless", params={"value": field_value}
254+
)
255+
else:
256+
# For ES7 and below, we need to filter documents explicitly
257+
update = update.filter("term", **{field_name: field_value})
258+
update = update.script(
259+
source=script_source,
260+
lang="painless",
261+
params={"value": field_value},
262+
conflicts="proceed",
263+
)
264+
265+
# refresh index to ensure search fetches all matches
266+
doc_type._index.refresh()
226267

227268
update.execute()
228269

@@ -238,6 +279,9 @@ def delete_object(doc_type_name, obj_id):
238279
kwargs = {}
239280
# For ES8, use string "true" instead of boolean True for refresh parameter
240281
if settings.TEST:
241-
kwargs["refresh"] = "true"
282+
if settings.ES_VERSION >= 8:
283+
kwargs["refresh"] = "true"
284+
else:
285+
kwargs["refresh"] = True
242286

243287
doc.to_action("delete", **kwargs)

kitsune/search/fields.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
from functools import partial
22

33
from django.conf import settings
4-
from elasticsearch.dsl.field import Keyword
5-
from elasticsearch.dsl.field import Object as DSLObject
6-
from elasticsearch.dsl.field import Text
4+
5+
if settings.ES_VERSION == 8:
6+
from elasticsearch8.dsl.field import Keyword
7+
from elasticsearch8.dsl.field import Object as DSLObject
8+
from elasticsearch8.dsl.field import Text
9+
else:
10+
from elasticsearch_dsl.field import Keyword
11+
from elasticsearch_dsl.field import Object as DSLObject
12+
from elasticsearch_dsl.field import Text
713

814
from kitsune.search.es_utils import es_analyzer_for_locale
915

kitsune/search/management/commands/es_init.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from django.core.management.base import BaseCommand
2-
from elasticsearch.dsl.exceptions import IllegalOperation
2+
from django.conf import settings
3+
4+
if settings.ES_VERSION == 8:
5+
from elasticsearch8.dsl.exceptions import IllegalOperation
6+
else:
7+
from elasticsearch_dsl.exceptions import IllegalOperation
38
from datetime import datetime, timezone
49

510
from kitsune.search.es_utils import get_doc_types, es_client
@@ -76,6 +81,9 @@ def handle(self, *args, **kwargs):
7681
index = dt.alias_points_at(dt.Index.write_alias)
7782
if kwargs["reload_search_analyzers"]:
7883
print(f"Reloading search analyzers on {index}")
79-
client.indices.reload_search_analyzers(index=[index])
84+
if settings.ES_VERSION >= 8:
85+
client.indices.reload_search_analyzers(index=[index])
86+
else:
87+
client.indices.reload_search_analyzer(index)
8088

8189
print("") # print blank line to make console output easier to read

kitsune/search/parser/operators.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
from elasticsearch.dsl import Q as DSLQ
1+
from django.conf import settings
2+
3+
if settings.ES_VERSION == 8:
4+
from elasticsearch8.dsl import Q as DSLQ
5+
else:
6+
from elasticsearch_dsl import Q as DSLQ
27

38
from .tokens import BaseToken
49

kitsune/search/parser/tokens.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from abc import ABC, abstractmethod
2+
from django.conf import settings
23

3-
from elasticsearch.dsl import Q as DSLQ
4+
if settings.ES_VERSION == 8:
5+
from elasticsearch8.dsl import Q as DSLQ
6+
else:
7+
from elasticsearch_dsl import Q as DSLQ
48

59

610
class BaseToken(ABC):

kitsune/search/search.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55

66
import bleach
77
from dateutil import parser
8+
from django.conf import settings
89
from django.utils.text import slugify
9-
from elasticsearch.dsl import Q as DSLQ
10+
11+
if settings.ES_VERSION == 8:
12+
from elasticsearch8.dsl import Q as DSLQ
13+
else:
14+
from elasticsearch_dsl import Q as DSLQ
1015

1116
from kitsune.products.models import Product
1217
from kitsune.search import HIGHLIGHT_TAG, SNIPPET_LENGTH

0 commit comments

Comments
 (0)